前言
How to play?
到 GitHub仓库 将整个文件克隆到本地。
在终端依次执行如下指令:
1 npm install
1 npx harhat play一个方便测试的 在线网站
1. puzzle_01
源码:
1 | ############ |
分析:
CALLVALUE
表示传入的msg.value
,其单位是wei
;
JUMP
则是表示调转到JUMPDEST
标志的位置,比如在本题中CALLVALUE
= 5,则会执行0x05
位置的代码,不出所料应该是会报错的。
所以本题只要发送
8
,则可以成功执行。
结果:
2. puzzle_02
源码:
1 | ############ |
分析:
CALLVALUE
和上一题一样,CODESIZE
则表示代码的大小,而从题目中不难看出09
表示代码大小为10
bytes,而CODESIZE
的单位也是bytes,所以CODESIZE
的值是10
。这里和逆波兰表达有点区别,这里的减法表示的是:当读取到
SUB
操作码时,EVM读取栈的顺序为:CODESIZE SUB CALLVALUE
即10 - CALLVALUE
,要使本题成功执行,则需跳转到06
,10 - CALLVALUE = 06
,所以不难得出CALLVALUE
= 4。
结果:
3. puzzle_03
源码:
1 | ############ |
分析:
还是为了跳转到被
JUMPDEST
标志的04
栈的位置,而CALLDATASIZE
则是统计calldata
数据的长度,所以只需随便发送一个4bytes
的数据即可,因为只能发送十六进制,且在十六进制中两位数字表示1bytes
,所以简单设置calldata = 0x00000000
即可。
结果:
4. puzzle_04
源码:
1 | ############ |
分析:
重点是
XOR
,其表示按位异或。不难看出CODESIZE
= 12,通关条件为CALLVALUE ^ CODESIZE = a = 10 = 1010
,通过简单的计算不难算出CALLVALUE
的值。
1
2
3 CODESIZE 1100
JUMPDEST 1010
CALLVALUE 0110所以发送
6
,即可通关。
结果:
5. puzzle_05
源码:
1 | ############ |
分析:
DUP1
:复制堆栈中的第一个值并将其推入堆栈的第一个位置(DUP2
:表示复制第二个值,并将其推入堆栈的第一个位置,其他同理);
MUL
:表示将前两个值进行相乘并压入栈;
PUSH2 0100
:表示将0100
这两个字节的值压入栈顶,PUSH1, PUSH3,PUSH4...
表示压入几个字节,最大压入32bytes
,即PUSH32
;
EQ
:弹出前两个值,比较其值是否相同,如果相同则压入1
,否则压入0
;
PUSH1 0C
:表示将一个字节的0c
压入栈顶;
JUMPI
:当JUMPI
执行时,它会从堆栈中弹出 2 个值。第一个值将是要跳转到的新程序计数器(一如既往,它必须是有效JUMPDEST
指令)。第二个值是一个布尔标志(0 或 1),用于评估是否必须跳转。如果值为1则跳转;否则继续执行下一条指令。综上,要是执行到
JUMPI
指令之前,栈中元素为00:0c 01:01
只要执行
CALLVALUE DUP1 MUL PUSH2 0100 EQ
之后,返回的值是true,即可通过(CALLVALUE ^ CALLVALUE == 0x0100 = 16 ^ 2 = 256),所以发送16
便可以通关。
结果:
6. puzzle_06
源码:
1 | ############ |
分析:
PUSH1 00
:往栈中压入0bytes
;
CALLDATALOAD
:表示从指定位置开始读取32bytes
的数据,题中则是表示偏移量为0
,即从索引为0
的坐标开始读取32bytes
的数据,读取到的数据的值为0a
,即可通关。
结果:
7. puzzle_07
源码:
1 | ############ |
分析:
p1:
1
2
3
4 00 36 CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY三个堆栈输入分别是
CALLDATACOPY
就像一个“特殊”MLOAD,直接从 calldata 位置获取要存储在内存中的数据。这些指令的意思是:从 calldata 中取出所有数据并将其复制到从内存位置 0 开始的内存中。p2:
1
2
3
4 05 36 CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE它的堆栈输入分别是:
value
:以Wei为单位的值发送到新帐户。offset
:内存中的字节偏移量(以字节为单位),新帐户的初始化代码。size
:要复制的字节大小(初始化代码的大小)。堆栈输出:
address
:已部署合约的地址,如果部署失败,则为 0。p3:
1
2
3
4
5 0B 3B EXTCODESIZE
0C 6001 PUSH1 01
0E 14 EQ
0F 6013 PUSH1 13
11 57 JUMPI
EXTCODESIZE
获取已部署合约的大小(以字节为单位)并将其添加到堆栈中。之后,谜题检查已部署合约的大小是否等于值 1。如果是,我们按照 到达该JUMPI
位置13
并赢得挑战。The solution is to find a
calldata
value for which the result ofEXTCODESIZE
(done on the contract deployed with code from thecalldata
itself) return 1.具体分析博客可见如下大佬博客:
重点:
When the
CREATE
opcode is executed, only the code returned by theRETURN
opcode will be the “runtime code” that will be executed in the future when the deployed contract will be called. The other part of the bytecode is just used once, only for theconstructor
part.*个人见解:通过
create
操作码创建的合约地址,[...] 创建代码在事务中执行,该事务返回运行时代码的副本,这是合约的实际代码。正如我们将看到的,构造函数是创建代码的一部分,而不是运行时代码的一部分。合约的构造函数是创建代码的一部分;一旦部署,它就不会出现在合约的代码中。
,实际上初始化合约的代码指令为执行calldata
之后,**通过执行calldata
中的RETURN
语句返回的代码才是将来调用部署的合约时执行的runtime code
*。分析
RETURN
:
- 从开始读取位置的内存偏移量
- 要读取和返回的内存大小(以字节为单位)
简单来说,其返回的值是从
memory
中读取的,从哪读取,读取多少取决于offset,size
。所以,只要按要求拼接自定义
calldata
即可,原则是:将一条指令写入memory
,且通过RETURN
从memory
中返回这一条指令用于初始化合约,这样一来,合约中便只有一条代码,EXTCODESIZE
返回的值便是1
。
1
2
3
4
5
6
7
8
9
10 ## 拼接calldata ##
## 将calldata写入memory
PUSH1 60 ff ## mstore'value,只要是一个字节即可
PUSH1 60 00 ## mstore'offset,在memory内存中的存储起始索引
## 这里采用MSTORE8,这在memory中的存储方式为:0xff00000000000...(32位)
MSTORE8 53 ## MSTORE8 操作码,在memory中写入1bytes
## 从memory中返回代码,用来执行合约的初始化
PUSH1 60 01 ## RETURN'size,从memory中读取代码的大小(长度)
PUSH1 60 00 ## RETURN'offset,从memory中读取代码的起始索引
RETURN f3 ## RETURN 操作码,从memory中返回runtime code所以,构造出来的
calldata
便是:
1 0x60ff60005360016000f3
结果: (如下这个结果同理也可以,0x60ff6000526001601ff3)
8. puzzle_08
源码:
1 | ############ |
分析:
p1:
1
2
3
4 00 36 CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY将
calldata
拷贝到memory
中。p2:
1
2
3
4 05 36 CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE根据拷贝在
memory
中的calldata
创建合约地址。p3:
1
2
3
4
5
6
7
8 0B 6000 PUSH1 00
0D 80 DUP1
0E 80 DUP1
0F 80 DUP1
10 80 DUP1
11 94 SWAP5
12 5A GAS
13 F1 CALL
SWAP5
:将栈顶的00
与CRETAE
创建的合约地址交换位置。分析
CALL
:
gas
: the amount of gas to send to the sub context created for the execution.address
: the address on which the context will be executedvalue
: value inwei
to send to the addressargsOffset
: byte offset in the memory in number of bytesargsSize
: byte size to copy from the memory with the previously specified offsetretOffset
: byte offset in memory in bytes from which you want to store the return data returned by the executionretSize
: byte size to copy from the returned datap4:
1
2
3
4 14 6000 PUSH1 00
16 14 EQ
17 601B PUSH1 1B
19 57 JUMPI即要求合约调用失败,
EQ
的返回值才为1
,程序才可以正确执行。所以只要本着调用失败去实现即可,又
FD REVERT
,所以可以在puzzles_7
的基础上进行修改即可。用于初始化的指令为
FD
,则有
1 calldata = 0x60fd60005360016000f3
结果:
9. puzzle_09
源码:
1 | ############ |
分析:
p1:
1
2
3
4
5 00 36 CALLDATASIZE
01 6003 PUSH1 03
03 10 LT
04 6009 PUSH1 09
06 57 JUMPI要求
calldata
的长度小于3bytes
(LT
表示小于)p2:
1
2
3
4
5
6
7 0A 34 CALLVALUE
0B 36 CALLDATASIZE
0C 02 MUL
0D 6008 PUSH1 08
0F 14 EQ
10 6014 PUSH1 14
12 57 JUMPI通过p1之后,要求`msg.value ^ calldatasize == 0x08。
简单,令
calldata = 0x00000006,msg.sender = 2
结果:
10. puzzle_10
源码:
1 | ############# |
分析:
p1:
1
2
3
4
5
6
7
8 00 38 CODESIZE
01 34 CALLVALUE
02 90 SWAP1
03 11 GT
04 6008 PUSH1 08
06 57 JUMPI
07 FD REVERT
08 5B JUMPDEST
CALLVALUE
和CODESIZE
交换位置,CODESIZE
= 0x1A = 26,要求msg.value
< 26。p2:
1
2
3
4
5
6
7
8
9 09 36 CALLDATASIZE
0A 610003 PUSH2 0003
0D 90 SWAP1
0E 06 MOD
0F 15 ISZERO
10 34 CALLVALUE
11 600A PUSH1 0A
13 01 ADD
14 57 JUMPI
ISZERO 要求: CALLDATASIEZE % 3 == 0
,且msg.value + 0x0a == 0x19 == 25
,所以msg.value=15
。故:
1
2 CALLDATASIEZE % 3 == 0;
msg.value == 15;所以: calldata 可为 0x000006
结果:
完结
参考链接